# Copyright (c) 2015-2025 Vector 35 Inc
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

import ctypes
import traceback
import webbrowser
from typing import Optional, Callable, List

# Binary Ninja components
from . import _binaryninjacore as core
from .enums import FormInputFieldType, MessageBoxIcon, MessageBoxButtonSet, MessageBoxButtonResult, ReportType
from . import binaryview
from .log import log_error_for_exception
from . import flowgraph
from . import mainthread


class LabelField:
	"""
	``LabelField`` adds a text label to the display.
	"""
	def __init__(self, text: str):
		self._text = text

	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.LabelFormField
		value.hasDefault = False
		value.prompt = self._text

	def _fill_core_result(self, value):
		pass

	def _get_result(self, value):
		pass

	@property
	def text(self) -> str:
		return self._text

	@text.setter
	def text(self, value: str) -> None:
		self._text = value


class SeparatorField:
	"""
	``SeparatorField`` adds vertical separation to the display.
	"""
	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.SeparatorFormField
		value.hasDefault = False

	def _fill_core_result(self, value):
		pass

	def _get_result(self, value):
		pass


class TextLineField:
	"""
	``TextLineField`` Adds prompt for text string input. Result is stored in self.result as a string on completion.
	"""
	def __init__(self, prompt: str, default: Optional[str] = None):
		self._prompt = prompt
		self._default = default
		self._result = None

	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.TextLineFormField
		value.prompt = self._prompt
		value.hasDefault = self._default is not None
		if self._default is not None:
			value.stringDefault = self._default

	def _fill_core_result(self, value):
		value.stringResult = core.BNAllocString(str(self._result))

	def _get_result(self, value):
		self._result = value.stringResult

	@property
	def prompt(self):
		return self._prompt

	@prompt.setter
	def prompt(self, value):
		self._prompt = value

	@property
	def result(self):
		return self._result

	@result.setter
	def result(self, value):
		self._result = value


class MultilineTextField:
	"""
	``MultilineTextField`` add multi-line text string input field. Result is stored in self.result
	as a string. This option is not supported on the command-line.
	"""
	def __init__(self, prompt: str, default: Optional[str] = None):
		self._prompt = prompt
		self._default = default
		self._result = None

	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.MultilineTextFormField
		value.prompt = self._prompt
		value.hasDefault = self._default is not None
		if self._default is not None:
			value.stringDefault = self._default

	def _fill_core_result(self, value):
		value.stringResult = core.BNAllocString(str(self._result))

	def _get_result(self, value):
		self._result = value.stringResult

	@property
	def prompt(self):
		return self._prompt

	@prompt.setter
	def prompt(self, value):
		self._prompt = value

	@property
	def result(self):
		return self._result

	@result.setter
	def result(self, value):
		self._result = value


class IntegerField:
	"""
	``IntegerField`` add prompt for integer. Result is stored in self.result as an int.
	"""
	def __init__(self, prompt: str, default: Optional[int] = None):
		self._prompt = prompt
		self._default = default
		self._result = None

	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.IntegerFormField
		value.prompt = self._prompt
		value.hasDefault = self._default is not None
		if self._default is not None:
			value.intDefault = self._default

	def _fill_core_result(self, value):
		value.intResult = self._result

	def _get_result(self, value):
		self._result = value.intResult

	@property
	def prompt(self):
		return self._prompt

	@prompt.setter
	def prompt(self, value):
		self._prompt = value

	@property
	def result(self):
		return self._result

	@result.setter
	def result(self, value):
		self._result = value


class AddressField:
	"""
	``AddressField`` prompts the user for an address. By passing the optional view and current_address parameters \
	offsets can be used instead of just an address. The result is stored as in int in self.result.

	.. note:: This API currently functions differently on the command-line, as the view and current_address are \
	disregarded. Additionally where as in the UI the result defaults to hexadecimal on the command-line 0x must be \
	specified.

	"""
	def __init__(self, prompt: str, view: Optional['binaryview.BinaryView'] = None, current_address: int = 0, default: Optional[int] = None):
		self._prompt = prompt
		self._view = view
		self._current_address = current_address
		self._default = default
		self._result = None

	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.AddressFormField
		value.prompt = self._prompt
		value.view = None
		if self._view is not None:
			value.view = self._view.handle
		value.currentAddress = self._current_address
		value.hasDefault = self._default is not None
		if self._default is not None:
			value.addressDefault = self._default

	def _fill_core_result(self, value):
		value.addressResult = self._result

	def _get_result(self, value):
		self._result = value.addressResult

	@property
	def prompt(self):
		"""prompt to be presented to the user"""
		return self._prompt

	@prompt.setter
	def prompt(self, value):
		self._prompt = value

	@property
	def view(self):
		"""BinaryView for the address"""
		return self._view

	@view.setter
	def view(self, value):
		self._view = value

	@property
	def current_address(self):
		"""current address to use as a base for relative calculations"""
		return self._current_address

	@current_address.setter
	def current_address(self, value):
		self._current_address = value

	@property
	def result(self):
		return self._result

	@result.setter
	def result(self, value):
		self._result = value


class ChoiceField:
	"""
	``ChoiceField`` prompts the user to choose from the list of strings provided in ``choices``. Result is stored \
	in self.result as an index in to the choices array.

	:param str prompt: Prompt to be presented to the user
	:param list(str) choices: List of choices to choose from
	:param Optional[int] default: Optional index into choices that will be selected by default
	"""
	def __init__(self, prompt: str, choices: List[str], default: Optional[int] = None):
		self._prompt = prompt
		self._choices = choices
		self._default = default
		self._result = None

	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.ChoiceFormField
		value.prompt = self._prompt
		choice_buf = (ctypes.c_char_p * len(self._choices))()
		for i in range(0, len(self._choices)):
			choice_buf[i] = self._choices[i].encode('charmap')
		value.choices = choice_buf
		value.count = len(self._choices)
		value.hasDefault = self._default is not None
		if self._default is not None:
			value.indexDefault = self._default

	def _fill_core_result(self, value):
		value.indexResult = self._result

	def _get_result(self, value):
		self._result = value.indexResult

	@property
	def prompt(self) -> str:
		return self._prompt

	@prompt.setter
	def prompt(self, value: str):
		self._prompt = value

	@property
	def choices(self) -> List[str]:
		return self._choices

	@choices.setter
	def choices(self, value: List[str]):
		self._choices = value

	@property
	def default(self) -> Optional[int]:
		return self._default

	@default.setter
	def default(self, value: Optional[int]):
		self._default = value

	@property
	def result(self) -> Optional[int]:
		return self._result

	@result.setter
	def result(self, value: int):
		self._result = value


class OpenFileNameField:
	"""
	``OpenFileNameField`` prompts the user to specify a file name to open. Result is stored in self.result as a string.
	"""
	def __init__(self, prompt: str, ext: str = "", default: Optional[str] = None):
		self._prompt = prompt
		self._ext = ext
		self._default = default
		self._result = None

	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.OpenFileNameFormField
		value.prompt = self._prompt
		value.ext = self._ext
		value.hasDefault = self._default is not None
		if self._default is not None:
			value.stringDefault = self._default

	def _fill_core_result(self, value):
		value.stringResult = core.BNAllocString(str(self.result))

	def _get_result(self, value):
		self._result = value.stringResult

	@property
	def prompt(self):
		return self._prompt

	@prompt.setter
	def prompt(self, value):
		self._prompt = value

	@property
	def ext(self):
		return self._ext

	@ext.setter
	def ext(self, value):
		self._ext = value

	@property
	def result(self):
		return self._result

	@result.setter
	def result(self, value):
		self._result = value


class SaveFileNameField:
	"""
	``SaveFileNameField`` prompts the user to specify a file name to save. Result is stored in self.result as a string.
	"""
	def __init__(self, prompt: str, ext: str = "", default_name: str = "", default: Optional[str] = None):
		self._prompt = prompt
		self._ext = ext
		self._default_name = default_name
		self._default = default
		self._result: Optional[str] = None

	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.SaveFileNameFormField
		value.prompt = self._prompt
		value.ext = self._ext
		value.defaultName = self._default_name
		value.hasDefault = self._default is not None
		if self._default is not None:
			value.stringDefault = self._default

	def _fill_core_result(self, value):
		value.stringResult = core.BNAllocString(str(self._result))

	def _get_result(self, value):
		self._result = value.stringResult

	@property
	def prompt(self):
		return self._prompt

	@prompt.setter
	def prompt(self, value: str):
		self._prompt = value

	@property
	def ext(self):
		return self._ext

	@ext.setter
	def ext(self, value: str):
		self._ext = value

	@property
	def default_name(self):
		return self._default_name

	@default_name.setter
	def default_name(self, value: str):
		self._default_name = value

	@property
	def result(self):
		return self._result

	@result.setter
	def result(self, value: Optional[str]):
		self._result = value


class DirectoryNameField:
	"""
	``DirectoryNameField`` prompts the user to specify a directory name to open. Result is stored in self.result as
	a string.
	"""
	def __init__(self, prompt: str, default_name: str = "", default: Optional[str] = None):
		self._prompt = prompt
		self._default_name = default_name
		self._default = default
		self._result = None

	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.DirectoryNameFormField
		value.prompt = self._prompt
		value.defaultName = self._default_name
		value.hasDefault = self._default is not None
		if self._default is not None:
			value.stringDefault = self._default

	def _fill_core_result(self, value):
		value.stringResult = core.BNAllocString(str(self._result))

	def _get_result(self, value):
		self._result = value.stringResult

	@property
	def prompt(self):
		return self._prompt

	@prompt.setter
	def prompt(self, value: str):
		self._prompt = value

	@property
	def default_name(self):
		return self._default_name

	@default_name.setter
	def default_name(self, value: str):
		self._default_name = value

	@property
	def result(self):
		return self._result

	@result.setter
	def result(self, value: Optional[str]):
		self._result = value


class CheckboxField:
	"""
	``CheckboxField`` prompts the user to choose a yes/no option in a checkbox.
	Result is stored in self.result as a boolean value.

	:param str prompt: Prompt to be presented to the user
	:param bool default: Default state of the checkbox (False == unchecked, True == checked)
	"""
	def __init__(self, prompt: str, default: Optional[bool]):
		self._prompt = prompt
		self._result = None
		self._default = default

	def _fill_core_struct(self, value):
		value.type = FormInputFieldType.CheckboxFormField
		value.prompt = self._prompt
		value.hasDefault = True
		value.intDefault = 1 if self._default else 0

	def _fill_core_result(self, value):
		value.intResult = 1 if self.result else 0

	def _get_result(self, value):
		self._result = value.intResult != 0

	@property
	def prompt(self):
		return self._prompt

	@prompt.setter
	def prompt(self, value: str):
		self._prompt = value

	@property
	def result(self):
		return self._result

	@result.setter
	def result(self, value: bool):
		self._result = value

	@property
	def default(self):
		return self._default

	@default.setter
	def default(self, value: Optional[bool]):
		self._default = value


class InteractionHandler:
	_interaction_handler = None

	def __init__(self):
		self._cb = core.BNInteractionHandlerCallbacks()
		self._cb.context = 0
		self._cb.showPlainTextReport = self._cb.showPlainTextReport.__class__(self._show_plain_text_report)
		self._cb.showMarkdownReport = self._cb.showMarkdownReport.__class__(self._show_markdown_report)
		self._cb.showHTMLReport = self._cb.showHTMLReport.__class__(self._show_html_report)
		self._cb.showGraphReport = self._cb.showGraphReport.__class__(self._show_graph_report)
		self._cb.showReportCollection = self._cb.showReportCollection.__class__(self._show_report_collection)
		self._cb.getTextLineInput = self._cb.getTextLineInput.__class__(self._get_text_line_input)
		self._cb.getIntegerInput = self._cb.getIntegerInput.__class__(self._get_int_input)
		self._cb.getAddressInput = self._cb.getAddressInput.__class__(self._get_address_input)
		self._cb.getChoiceInput = self._cb.getChoiceInput.__class__(self._get_choice_input)
		self._cb.getLargeChoiceInput = self._cb.getLargeChoiceInput.__class__(self._get_large_choice_input)
		self._cb.getOpenFileNameInput = self._cb.getOpenFileNameInput.__class__(self._get_open_filename_input)
		self._cb.getSaveFileNameInput = self._cb.getSaveFileNameInput.__class__(self._get_save_filename_input)
		self._cb.getDirectoryNameInput = self._cb.getDirectoryNameInput.__class__(self._get_directory_name_input)
		self._cb.getCheckboxInput = self._cb.getCheckboxInput.__class__(self._get_checkbox_input)
		self._cb.getFormInput = self._cb.getFormInput.__class__(self._get_form_input)
		self._cb.showMessageBox = self._cb.showMessageBox.__class__(self._show_message_box)
		self._cb.openUrl = self._cb.openUrl.__class__(self._open_url)
		self._cb.runProgressDialog = self._cb.runProgressDialog.__class__(self._run_progress_dialog)

	def register(self):
		self.__class__._interaction_handler = self
		core.BNRegisterInteractionHandler(self._cb)

	def _show_plain_text_report(self, ctxt, view, title, contents):
		try:
			if view:
				view = binaryview.BinaryView(handle=core.BNNewViewReference(view))
			else:
				view = None
			self.show_plain_text_report(view, title, contents)
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._show_plain_text_report")

	def _show_markdown_report(self, ctxt, view, title, contents, plaintext):
		try:
			if view:
				view = binaryview.BinaryView(handle=core.BNNewViewReference(view))
			else:
				view = None
			self.show_markdown_report(view, title, contents, plaintext)
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._show_markdown_report")

	def _show_html_report(self, ctxt, view, title, contents, plaintext):
		try:
			if view:
				view = binaryview.BinaryView(handle=core.BNNewViewReference(view))
			else:
				view = None
			self.show_html_report(view, title, contents, plaintext)
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._show_html_report")

	def _show_graph_report(self, ctxt, view, title, graph):
		try:
			if view:
				view = binaryview.BinaryView(handle=core.BNNewViewReference(view))
			else:
				view = None
			self.show_graph_report(view, title, flowgraph.CoreFlowGraph(core.BNNewFlowGraphReference(graph)))
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._show_graph_report")

	def _show_report_collection(self, ctxt, title, reports):
		try:
			self.show_report_collection(title, ReportCollection(core.BNNewReportCollectionReference(reports)))
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._show_report_collection")

	def _get_text_line_input(self, ctxt, result, prompt, title):
		try:
			value = self.get_text_line_input(prompt, title)
			if value is None:
				return False
			result[0] = core.BNAllocString(str(value))
			return True
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._get_text_line_input")

	def _get_int_input(self, ctxt, result, prompt, title):
		try:
			value = self.get_int_input(prompt, title)
			if value is None:
				return False
			result[0] = value
			return True
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._get_int_input")

	def _get_address_input(self, ctxt, result, prompt, title, view, current_address):
		try:
			if view:
				view = binaryview.BinaryView(handle=core.BNNewViewReference(view))
			else:
				view = None
			value = self.get_address_input(prompt, title, view, current_address)
			if value is None:
				return False
			result[0] = value
			return True
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._get_address_input")

	def _get_choice_input(self, ctxt, result, prompt, title, choice_buf, count):
		try:
			choices = []
			for i in range(0, count):
				choices.append(choice_buf[i])
			value = self.get_choice_input(prompt, title, choices)
			if value is None:
				return False
			result[0] = value
			return True
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._get_choice_input")

	def _get_large_choice_input(self, ctxt, result, prompt, title, choice_buf, count):
		try:
			choices = []
			for i in range(0, count):
				choices.append(choice_buf[i])
			value = self.get_large_choice_input(prompt, title, choices)
			if value is None:
				return False
			result[0] = value
			return True
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._get_large_choice_input")

	def _get_open_filename_input(self, ctxt, result, prompt, ext):
		try:
			value = self.get_open_filename_input(prompt, ext)
			if value is None:
				return False
			result[0] = core.BNAllocString(str(value))
			return True
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._get_open_filename_input")

	def _get_save_filename_input(self, ctxt, result, prompt, ext, default_name):
		try:
			value = self.get_save_filename_input(prompt, ext, default_name)
			if value is None:
				return False
			result[0] = core.BNAllocString(str(value))
			return True
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._get_save_filename_input")

	def _get_directory_name_input(self, ctxt, result, prompt, default_name):
		try:
			value = self.get_directory_name_input(prompt, default_name)
			if value is None:
				return False
			result[0] = core.BNAllocString(str(value))
			return True
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._get_directory_name_input")

	def _get_checkbox_input(self, ctxt, result, prompt, default_choice):
		try:
			value = self.get_checkbox_input(prompt, default_choice)
			if value is None:
				return False
			result[0] = value
			return True
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._get_checkbox_input")

	def _get_form_input(self, ctxt, fields, count, title):
		try:
			field_objs = []
			for i in range(0, count):
				if fields[i].type == FormInputFieldType.LabelFormField:
					field_objs.append(LabelField(fields[i].prompt))
				elif fields[i].type == FormInputFieldType.SeparatorFormField:
					field_objs.append(SeparatorField())
				elif fields[i].type == FormInputFieldType.TextLineFormField:
					field_objs.append(
					    TextLineField(
					        fields[i].prompt, default=fields[i].stringDefault if fields[i].hasDefault else None
					    )
					)
				elif fields[i].type == FormInputFieldType.MultilineTextFormField:
					field_objs.append(
					    MultilineTextField(
					        fields[i].prompt, default=fields[i].stringDefault if fields[i].hasDefault else None
					    )
					)
				elif fields[i].type == FormInputFieldType.IntegerFormField:
					field_objs.append(
					    IntegerField(fields[i].prompt, default=fields[i].intDefault if fields[i].hasDefault else None)
					)
				elif fields[i].type == FormInputFieldType.AddressFormField:
					view = None
					if fields[i].view:
						view = binaryview.BinaryView(handle=core.BNNewViewReference(fields[i].view))
					field_objs.append(
					    AddressField(
					        fields[i].prompt, view, fields[i].currentAddress,
					        default=fields[i].addressDefault if fields[i].hasDefault else None
					    )
					)
				elif fields[i].type == FormInputFieldType.ChoiceFormField:
					choices = []
					for j in range(0, fields[i].count):
						choices.append(fields[i].choices[j])
					field_objs.append(
					    ChoiceField(
					        fields[i].prompt, choices, default=fields[i].choiceDefault if fields[i].hasDefault else None
					    )
					)
				elif fields[i].type == FormInputFieldType.OpenFileNameFormField:
					field_objs.append(
					    OpenFileNameField(
					        fields[i].prompt, fields[i].ext,
					        default=fields[i].stringDefault if fields[i].hasDefault else None
					    )
					)
				elif fields[i].type == FormInputFieldType.SaveFileNameFormField:
					field_objs.append(
					    SaveFileNameField(
					        fields[i].prompt, fields[i].ext, fields[i].defaultName,
					        default=fields[i].stringDefault if fields[i].hasDefault else None
					    )
					)
				elif fields[i].type == FormInputFieldType.DirectoryNameFormField:
					field_objs.append(
					    DirectoryNameField(
					        fields[i].prompt, fields[i].defaultName,
					        default=fields[i].stringDefault if fields[i].hasDefault else None
					    )
					)
				elif fields[i].type == FormInputFieldType.CheckboxFormField:
					field_objs.append(
					    CheckboxField(
					        fields[i].prompt,
							default=fields[i].intDefault != 0 if fields[i].hasDefault else None
					    )
					)
				else:
					field_objs.append(LabelField(fields[i].prompt))
			if not self.get_form_input(field_objs, title):
				return False
			for i in range(0, count):
				field_objs[i]._fill_core_result(fields[i])
			return True
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._get_form_input")

	def _show_message_box(self, ctxt, title, text, buttons, icon):
		try:
			return self.show_message_box(title, text, buttons, icon)
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._show_message_box")

	def _open_url(self, ctxt, url):
		try:
			return self.open_url(url)
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._open_url")
			return False

	def _run_progress_dialog(self, title, can_cancel, task, task_ctxt):
		try:
			def py_task(progress: Callable[[int, int], bool]):
				progress_c = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_size_t, lambda ctxt, cur, max: progress(cur, max))
				task(task_ctxt, progress_c, None)

			return self.run_progress_dialog(title, can_cancel, py_task)
		except:
			log_error_for_exception("Unhandled Python exception in InteractionHandler._run_progress_dialog")
			return False

	def show_plain_text_report(self, view, title, contents):
		pass

	def show_markdown_report(self, view, title, contents, plaintext):
		self.show_html_report(view, title, markdown_to_html(contents), plaintext)

	def show_html_report(self, view, title, contents, plaintext):
		if len(plaintext) != 0:
			self.show_plain_text_report(view, title, plaintext)

	def show_graph_report(self, view, title, graph):
		pass

	def show_report_collection(self, title, reports):
		pass

	def get_text_line_input(self, prompt, title):
		return NotImplemented

	def get_int_input(self, prompt, title):
		while True:
			text = self.get_text_line_input(prompt, title)
			if len(text) == 0:
				return False
			try:
				return int(text)
			except:
				continue

	def get_address_input(self, prompt, title, view, current_address):
		return get_int_input(prompt, title)

	def get_choice_input(self, prompt, title, choices):
		return NotImplemented

	def get_large_choice_input(self, prompt, title, choices):
		return NotImplemented

	def get_open_filename_input(self, prompt, ext):
		return get_text_line_input(prompt, "Open File")

	def get_save_filename_input(self, prompt, ext, default_name):
		return get_text_line_input(prompt, "Save File")

	def get_directory_name_input(self, prompt, default_name):
		return get_text_line_input(prompt, "Select Directory")

	def get_checkbox_input(self, prompt, default_choice):
		return get_checkbox_input(prompt, "Choose Option(s)", default_choice)

	def get_form_input(self, fields, title):
		return False

	def show_message_box(self, title, text, buttons, icon):
		return MessageBoxButtonResult.CancelButton

	def open_url(self, url):
		webbrowser.open(url)
		return True

	def run_progress_dialog(self, task: Callable[[Callable[[int, int], bool]], None]) -> bool:
		mainthread.execute_on_main_thread_and_wait(lambda: task(lambda cur, max: True))
		return True


class PlainTextReport:
	def __init__(self, title: str, contents: str, view: Optional['binaryview.BinaryView'] = None):
		self._view = view
		self._title = title
		self._contents = contents

	def __repr__(self):
		return "<plaintext report: %s>" % self._title

	def __str__(self):
		return self._contents

	@property
	def view(self):
		return self._view

	@view.setter
	def view(self, value: Optional['binaryview.BinaryView']):
		self._view = value

	@property
	def title(self):
		return self._title

	@title.setter
	def title(self, value: str):
		self._title = value

	@property
	def contents(self):
		return self._contents

	@contents.setter
	def contents(self, value: str):
		self._contents = value


class MarkdownReport:
	def __init__(self, title: str, contents: str, plaintext: str = "", view: Optional['binaryview.BinaryView'] = None):
		self._view = view
		self._title = title
		self._contents = contents
		self._plaintext = plaintext

	def __repr__(self):
		return "<markdown report: %s>" % self._title

	def __str__(self):
		return self._contents

	@property
	def view(self):
		return self._view

	@view.setter
	def view(self, value: Optional['binaryview.BinaryView']):
		self._view = value

	@property
	def title(self):
		return self._title

	@title.setter
	def title(self, value: str):
		self._title = value

	@property
	def contents(self):
		return self._contents

	@contents.setter
	def contents(self, value: str):
		self._contents = value

	@property
	def plaintext(self):
		return self._plaintext

	@plaintext.setter
	def plaintext(self, value: str):
		self._plaintext = value


class HTMLReport:
	def __init__(self, title: str, contents: str, plaintext: str = "", view: Optional['binaryview.BinaryView'] = None):
		self._view = view
		self._title = title
		self._contents = contents
		self._plaintext = plaintext

	def __repr__(self):
		return "<html report: %s>" % self._title

	def __str__(self):
		return self._contents

	@property
	def view(self):
		return self._view

	@view.setter
	def view(self, value: Optional['binaryview.BinaryView']):
		self._view = value

	@property
	def title(self):
		return self._title

	@title.setter
	def title(self, value: str):
		self._title = value

	@property
	def contents(self):
		return self._contents

	@contents.setter
	def contents(self, value: str):
		self._contents = value

	@property
	def plaintext(self):
		return self._plaintext

	@plaintext.setter
	def plaintext(self, value: str):
		self._plaintext = value


class FlowGraphReport:
	def __init__(self, title: str, graph: 'flowgraph.FlowGraph', view: Optional['binaryview.BinaryView'] = None):
		self._view = view
		self._title = title
		self._graph = graph

	def __repr__(self):
		return "<graph report: %s>" % self._title

	@property
	def view(self):
		return self._view

	@view.setter
	def view(self, value: Optional['binaryview.BinaryView']):
		self._view = value

	@property
	def title(self):
		return self._title

	@title.setter
	def title(self, value: str):
		self._title = value

	@property
	def graph(self):
		return self._graph

	@graph.setter
	def graph(self, value: 'flowgraph.FlowGraph'):
		self._graph = value


class ReportCollection:
	def __init__(self, handle=None):
		if handle is None:
			self.handle = core.BNCreateReportCollection()
		else:
			self.handle = handle

	def __len__(self):
		return core.BNGetReportCollectionCount(self.handle)

	def _report_from_index(self, i):
		report_type = core.BNGetReportType(self.handle, i)
		title: str = core.BNGetReportTitle(self.handle, i) # type: ignore
		view = core.BNGetReportView(self.handle, i)
		if view:
			view = binaryview.BinaryView(handle=view)
		else:
			view = None
		if report_type == ReportType.PlainTextReportType:
			contents: str = core.BNGetReportContents(self.handle, i) # type: ignore
			return PlainTextReport(title, contents, view)
		elif report_type == ReportType.MarkdownReportType:
			contents: str = core.BNGetReportContents(self.handle, i) # type: ignore
			plaintext: str = core.BNGetReportPlainText(self.handle, i) # type: ignore
			return MarkdownReport(title, contents, plaintext, view)
		elif report_type == ReportType.HTMLReportType:
			contents: str = core.BNGetReportContents(self.handle, i) # type: ignore
			plaintext: str = core.BNGetReportPlainText(self.handle, i) # type: ignore
			return HTMLReport(title, contents, plaintext, view)
		elif report_type == ReportType.FlowGraphReportType:
			graph = flowgraph.CoreFlowGraph(core.BNGetReportFlowGraph(self.handle, i))
			return FlowGraphReport(title, graph, view)
		raise TypeError("invalid report type %s" % repr(report_type))

	def __getitem__(self, i):
		if isinstance(i, slice) or isinstance(i, tuple):
			raise IndexError("expected integer report index")
		if (i < 0) or (i >= len(self)):
			raise IndexError("index out of range")
		return self._report_from_index(i)

	def __iter__(self):
		count = len(self)
		for i in range(0, count):
			yield self._report_from_index(i)

	def __repr__(self):
		return "<reports: %s>" % repr(list(self))

	def append(self, report):
		if report.view is None:
			view = None
		else:
			view = report.view.handle
		if isinstance(report, PlainTextReport):
			core.BNAddPlainTextReportToCollection(self.handle, view, report.title, report.contents)
		elif isinstance(report, MarkdownReport):
			core.BNAddMarkdownReportToCollection(self.handle, view, report.title, report.contents, report.plaintext)
		elif isinstance(report, HTMLReport):
			core.BNAddHTMLReportToCollection(self.handle, view, report.title, report.contents, report.plaintext)
		elif isinstance(report, FlowGraphReport):
			core.BNAddGraphReportToCollection(self.handle, view, report.title, report.graph.handle)
		else:
			raise TypeError("expected report object")

	def update(self, i, report):
		# if isinstance(report, PlainTextReport):
		# 	core.BNUpdatePlainTextReportToCollection(self.handle, i, report.contents)
		# elif isinstance(report, MarkdownReport):
		# 	core.BNUpdateMarkdownReportToCollection(self.handle, i, report.contents, report.plaintext)
		# elif isinstance(report, HTMLReport):
		# 	core.BNUpdateHTMLReportToCollection(self.handle, i, report.contents, report.plaintext)
		if isinstance(report, FlowGraphReport):
			core.BNUpdateReportFlowGraph(self.handle, i, report.graph.handle)
		else:
			raise TypeError("expected report object")


def markdown_to_html(contents):
	"""
	``markdown_to_html`` converts the provided markdown to HTML

	:param str contents: Markdown contents to convert to HTML
	:rtype: str
	:Example:
		>>> markdown_to_html("##Yay")
		'<h2>Yay</h2>'
	"""
	return core.BNMarkdownToHTML(contents)


def show_plain_text_report(title, contents):
	"""
	``show_plain_text_report`` displays contents to the user in the UI or on the command-line

	.. note:: This API functions differently on the command-line vs the UI. In the UI, a pop-up is used. On the command-line, \
	a simple text prompt is used.

	:param str title: Title to display in the tab
	:param str contents: Plaintext contents to display
	:rtype: None
	:Example:
		>>> show_plain_text_report("title", "contents")
		contents
	"""
	core.BNShowPlainTextReport(None, title, contents)


def show_markdown_report(title, contents, plaintext=""):
	"""
	``show_markdown_report`` displays the markdown contents in UI applications and plaintext in command-line \
	applications. This API doesn't support hyperlinking into the BinaryView, use the \
	:py:meth:`BinaryView.show_markdown_report` API if hyperlinking is needed.

	.. note:: This API function differently on the command-line vs the UI. In the UI a pop-up is used. On the command-line \
	a simple text prompt is used.

	:param str title: title to display in the tab
	:param str contents: markdown contents to display
	:param str plaintext: Plain text version to display (used on the command-line)
	:rtype: None
	:Example:
		>>> show_markdown_report("title", "##Contents", "Plain text contents")
		Plain text contents
	"""
	core.BNShowMarkdownReport(None, title, contents, plaintext)


def show_html_report(title, contents, plaintext=""):
	"""
	``show_html_report`` displays the HTML contents in UI applications and plaintext in command-line \
	applications. This API doesn't support hyperlinking into the BinaryView, use the :py:meth:`BinaryView.show_html_report` \
	API if hyperlinking is needed.

	:param str title: Title to display in the tab
	:param str contents: HTML contents to display
	:param str plaintext: Plain text version to display (used on the command-line)
	:rtype: None
	:Example:
		>>> show_html_report("title", "<h1>Contents</h1>", "Plain text contents")
		Plain text contents
	"""
	core.BNShowHTMLReport(None, title, contents, plaintext)


def show_graph_report(title, graph):
	"""
	``show_graph_report`` displays a flow graph in UI applications and nothing in command-line applications. \
	This API doesn't support clickable references into an existing BinaryView. Use the :py:meth:`BinaryView.show_html_report` \
	API if hyperlinking is needed.

	.. note:: This API function will have no effect outside the UI.

	:param str title: Title to display in the tab
	:param FlowGraph graph: Flow graph to display
	:rtype: None
	"""
	func = graph.function
	if func is None:
		core.BNShowGraphReport(None, title, graph.handle)
	else:
		core.BNShowGraphReport(func.view.handle, title, graph.handle)


def show_report_collection(title, reports):
	"""
	``show_report_collection`` displays multiple reports in UI applications

	.. note:: This API function will have no effect outside the UI.

	:param ReportCollection reports: Reports to display
	:rtype: None
	"""
	core.BNShowReportCollection(title, reports.handle)


def get_text_line_input(prompt, title):
	"""
	``get_text_line_input`` prompts the user to input a string with the given prompt and title

	.. note:: This API function differently on the command-line vs the UI. In the UI a pop-up is used. On the command-line \
	a simple text prompt is used.

	:param str prompt: String to prompt with
	:param str title: Title of the window when executed in the UI
	:rtype: str containing the input without trailing newline character
	:Example:
		>>> get_text_line_input("PROMPT>", "getinfo")
		PROMPT> Input!
		'Input!'
	"""
	value = ctypes.c_char_p()
	if not core.BNGetTextLineInput(value, prompt, title):
		return None
	result = value.value
	core.free_string(value)
	return result


def get_int_input(prompt, title):
	"""
	``get_int_input`` prompts the user to input a integer with the given prompt and title

	.. note:: This API function differently on the command-line vs the UI. In the UI a pop-up is used. On the command-line \
	a simple text prompt is used.

	:param str prompt: String to prompt with
	:param str title: Title of the window when executed in the UI
	:rtype: integer value input by the user
	:Example:
		>>> get_int_input("PROMPT>", "getinfo")
		PROMPT> 10
		10
	"""
	value = ctypes.c_longlong()
	if not core.BNGetIntegerInput(value, prompt, title):
		return None
	return value.value


def get_address_input(prompt, title):
	"""
	``get_address_input`` prompts the user for an address with the given prompt and title

	.. note:: This API function differently on the command-line vs the UI. In the UI a pop-up is used. On the command-line \
	a simple text prompt is used.

	:param str prompt: String to prompt with.
	:param str title: Title of the window when executed in the UI.
	:rtype: integer value input by the user.
	:Example:
		>>> get_address_input("PROMPT>", "getinfo")
		PROMPT> 10
		10L
	"""
	value = ctypes.c_ulonglong()
	if not core.BNGetAddressInput(value, prompt, title, None, 0):
		return None
	return value.value


def get_choice_input(prompt, title, choices):
	"""
	``get_choice_input`` prompts the user to select the one of the provided choices

	.. note:: This API function differently on the command-line vs the UI. In the UI a pop-up is used. On the command-line \
	a simple text prompt is used. The UI uses a combo box.

	:param str prompt: String to prompt with.
	:param str title: Title of the window when executed in the UI.
	:param choices: A list of strings for the user to choose from.
	:type choices: list(str)
	:rtype: integer array index of the selected option
	:Example:
		>>> get_choice_input("PROMPT>", "choices", ["Yes", "No", "Maybe"])
		choices
		1) Yes
		2) No
		3) Maybe
		PROMPT> 1
		0L
	"""
	choice_buf = (ctypes.c_char_p * len(choices))()
	for i in range(0, len(choices)):
		choice_buf[i] = str(choices[i]).encode('charmap')
	value = ctypes.c_ulonglong()
	if not core.BNGetChoiceInput(value, prompt, title, choice_buf, len(choices)):
		return None
	return value.value


def get_large_choice_input(prompt, title, choices):
	"""
	``get_large_choice_input`` prompts the user to select the one of the provided choices from a large pool

	.. note:: This API function differently on the command-line vs the UI. In the UI a pop-up is used. On the command-line \
	a text prompt is used. The UI uses a filterable list of entries

	:param str prompt: Text for the button when executed in the UI. Prompt shown for selection headless.
	:param str title: Title of the window when executed in the UI.
	:param choices: A list of strings for the user to choose from.
	:type choices: list(str)
	:rtype: integer array index of the selected option
	:Example:
		>>> get_large_choice_input("Select Function", "Select a Function", [f.symbol.short_name for f in bv.functions])
	"""
	choice_buf = (ctypes.c_char_p * len(choices))()
	for i in range(0, len(choices)):
		choice_buf[i] = str(choices[i]).encode('charmap')
	value = ctypes.c_ulonglong()
	if not core.BNGetLargeChoiceInput(value, prompt, title, choice_buf, len(choices)):
		return None
	return value.value


def get_open_filename_input(prompt: str, ext: str = "") -> Optional[str]:
	"""
	``get_open_filename_input`` prompts the user for a file name to open

	.. note:: This API functions differently on the command-line vs the UI. In the UI a pop-up is used. On the command-line \
	a simple text prompt is used. The UI uses the native window pop-up for file selection.

	Multiple file selection groups can be included if separated by two semicolons. Multiple file wildcards may be specified by using a space within the parenthesis.

	Also, a simple selector of `*.extension` by itself may also be used instead of specifying the description.

	:param str prompt: Prompt to display.
	:param str ext: Optional, file extension
	:Example:
		>>> get_open_filename_input("filename:", "*.py")
		'test.py'
		>>> get_open_filename_input("filename:", "All Files (*)")
		'test.py'
		>>> get_open_filename_input("filename:", "Executables (*.exe)")
		'foo.exe'
		>>> get_open_filename_input("filename:", "Executables (*.exe *.com)")
		'foo.exe'
		>>> get_open_filename_input("filename:", "Executables (*.exe *.com);;Python Files (*.py);;All Files (*)")
		'foo.exe'
	"""
	value = ctypes.c_char_p()
	if not core.BNGetOpenFileNameInput(value, prompt, ext):
		return None
	result = value.value
	assert result is not None
	core.free_string(value)
	return result.decode("utf-8")


def get_save_filename_input(prompt: str, ext: str = "", default_name: str = "") -> Optional[str]:
	"""
	``get_save_filename_input`` prompts the user for a file name to save as, optionally providing a file extension and \
	default_name

	.. note:: This API function differently on the command-line vs the UI. In the UI a pop-up is used. On the command-line \
	a simple text prompt is used. The UI uses the native window pop-up for file selection.

	:param str prompt: Prompt to display.
	:param str ext: Optional, file extension
	:param str default_name: Optional, default file name.
	:Example:
		>>> get_save_filename_input("filename:", "*.py", "test.py")
		filename: test.py
		'test.py'
		>>> get_save_filename_input("filename:", "All Files (*)", "test.py")
		filename: test.py
		'test.py'
		>>> get_save_filename_input("filename:", "Executables (*.exe)", "foo.exe")
		filename: foo.exe
		'foo.exe'
		>>> get_save_filename_input("filename:", "Executables (*.exe *.com)", "foo.exe")
		filename: foo.exe
		'foo.exe'
		>>> get_save_filename_input("filename:", "Executables (*.exe *.com);;Python Files (*.py);;All Files (*)", "foo.exe")
		filename: foo.exe
		'foo.exe'
	"""
	value = ctypes.c_char_p()
	if not core.BNGetSaveFileNameInput(value, prompt, ext, default_name):
		return None
	result = value.value
	assert result is not None
	core.free_string(value)
	return result.decode("utf-8")


def get_directory_name_input(prompt: str, default_name: str = ""):
	"""
	``get_directory_name_input`` prompts the user for a directory name to save as, optionally providing a default_name

	.. note:: This API function differently on the command-line vs the UI. In the UI a pop-up is used. On the command-line a simple text prompt is used. The UI uses the native window pop-up for file selection.

	:param str prompt: Prompt to display.
	:param str default_name: Optional, default directory name.
	:rtype: str
	:Example:
		>>> get_directory_name_input("prompt")
		prompt dirname
		'dirname'
	"""
	value = ctypes.c_char_p()
	if not core.BNGetDirectoryNameInput(value, prompt, default_name):
		return None
	result = value.value
	assert result is not None
	core.free_string(value)
	return result.decode("utf-8")

def get_checkbox_input(prompt: str, title: str, default: bool = False):
	"""
	``get_checkbox_input`` prompts the user for a checkbox input
	:param prompt: String to prompt with
	:param title: Title of the window when executed in the UI
	:param default: Optional default state for the checkbox (false == unchecked, true == checked), False if not set.
	:rtype: bool indicating the state of the checkbox
	"""
	default_state = ctypes.c_int64()
	default_state.value = 1 if default else 0
	value = ctypes.c_int64()
	if not core.BNGetCheckboxInput(value, prompt, title, default_state):
		return None
	result = value.value
	assert result is not None
	return result != 0

def get_form_input(fields, title):
	"""
	``get_from_input`` Prompts the user for a set of inputs specified in ``fields`` with given title. \
	The fields parameter is a list which can contain the following types:

	===================== ===================================================
	FieldType             Description
	===================== ===================================================
	str                   an alias for LabelField
	None                  an alias for SeparatorField
	LabelField            Text output
	SeparatorField        Vertical spacing
	TextLineField         Prompt for a string value
	MultilineTextField    Prompt for multi-line string value
	IntegerField          Prompt for an integer
	AddressField          Prompt for an address
	ChoiceField           Prompt for a choice from provided options
	OpenFileNameField     Prompt for file to open
	SaveFileNameField     Prompt for file to save to
	DirectoryNameField    Prompt for directory name
	CheckboxFormField     Prompt for a checkbox
	===================== ===================================================

	This API is flexible and works both in the UI via a pop-up dialog and on the command-line.

	.. note:: More complicated APIs should consider using the included pyside2 functionality in the `binaryninjaui` module. Returns true or false depending on whether the user submitted responses or cancelled the dialog.

	:param fields: A list containing these classes, strings or None
	:type fields: list(str) or list(None) or list(LabelField) or list(SeparatorField) or list(TextLineField) or list(MultilineTextField) or list(IntegerField) or list(AddressField) or list(ChoiceField) or list(OpenFileNameField) or list(SaveFileNameField) or list(DirectoryNameField)
	:param str title: The title of the pop-up dialog
	:rtype: bool
	:Example:

		>>> int_f = IntegerField("Specify Integer")
		>>> tex_f = TextLineField("Specify name")
		>>> choice_f = ChoiceField("Options", ["Yes", "No", "Maybe"])
		>>> get_form_input(["Get Data", None, int_f, tex_f, choice_f], "The options")
		Get Data
		<empty>
		Specify Integer 1337
		Specify name Peter
		The options
		1) Yes
		2) No
		3) Maybe
		Options 1
		>>> True
		>>> print(tex_f.result, int_f.result, choice_f.result)
		Peter 1337 0
	"""
	value = (core.BNFormInputField * len(fields))()
	for i in range(0, len(fields)):
		if isinstance(fields[i], str):
			LabelField(fields[i])._fill_core_struct(value[i])
		elif fields[i] is None:
			SeparatorField()._fill_core_struct(value[i])
		else:
			fields[i]._fill_core_struct(value[i])
	if not core.BNGetFormInput(value, len(fields), title):
		return False
	for i in range(0, len(fields)):
		if not (isinstance(fields[i], str) or (fields[i] is None)):
			fields[i]._get_result(value[i])
	core.BNFreeFormInputResults(value, len(fields))
	return True


def show_message_box(title, text, buttons=MessageBoxButtonSet.OKButtonSet, icon=MessageBoxIcon.InformationIcon):
	"""
	``show_message_box`` Displays a configurable message box in the UI, or prompts on the console as appropriate

	:note: This uses a standard QDialog which means simple HTML will render as HTML, but links are not clickable and special characters need to be escaped.

	:param str title: Text title for the message box.
	:param str text: Text for the main body of the message box.
	:param MessageBoxButtonSet buttons: One of :py:class:`MessageBoxButtonSet`
	:param MessageBoxIcon icon: One of :py:class:`MessageBoxIcon`
	:return: Which button was selected
	:rtype: MessageBoxButtonResult
	"""
	return core.BNShowMessageBox(title, text, buttons, icon)


def open_url(url):
	"""
	``open_url`` Opens a given url in the user's web browser, if available.

	:param str url: Url to open
	:return: True if successful
	:rtype: bool
	"""
	return core.BNOpenUrl(url)


def run_progress_dialog(title: str, can_cancel: bool, task: Callable[[Callable[[int, int], bool]], None]) -> bool:
	"""
	``run_progress_dialog`` runs a given task in a background thread, showing an updating
	progress bar which the user can cancel.

	:param title: Dialog title
	:param can_cancel: If the task can be cancelled
	:param task: Function to perform the task, taking as a parameter a function which should be called to report progress updates and check for cancellation. If the progress function returns false, the user has requested to cancel, and the task should handle this appropriately.
	:return: True if not cancelled
	"""

	progress_type = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_size_t)
	task_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, progress_type, ctypes.c_void_p)

	def do_task(ctxt: ctypes.c_void_p, progress: progress_type, progress_ctxt: ctypes.c_void_p):
		def py_progress(cur: int, max: int) -> bool:
			return progress(progress_ctxt, cur, max)
		task(py_progress)

	return core.BNRunProgressDialog(title, can_cancel, task_type(do_task), None)
